/**
 * @namespace Robust AJAX object. Includes concepts from http://adamv.com/dev/
 * and http://extjs.com/ as well as JDOMPer.
 */
Jelo.Ajax = function() {
    
    var d = window.document;
    
    var Status = {
        OK          : 200,
        Created     : 201,
        Accepted    : 202,
        NoContent   : 204,
        BadRequest  : 400,
        Forbidden   : 403,
        NotFound    : 404,
        Gone        : 410,
        ServerError : 500
    };
    
    var cache = {};
    
    var getFromCache = function(/* url */u, /* success */s, /* failure */f, /* callback */fn, /* config */c) {
        var /* responseText */r = cache[u] || false, /* http */h;
        if (r) {
            h = {
                status       : Status.OK,
                readyState   : 4,
                responseText : r
            };
            if (s.isFunction) {
                s.apply(h, [h, c]);
            }
            if (fn.isFunction) {
                fn.apply(h, [h, c]);
            }
            return true;
        }
        if (f.isFunction) {
            f.apply(h, [h, c]);
        }
        return false;
    };
    
    /** @private http://www.ajaxpatterns.org/ */
    var x = function() {
        try {
            return new XMLHttpRequest();
        } catch (e) {}
        try {
            return new ActiveXObject("Msxml2.XMLHTTP");
        } catch (f) {}
        try {
            return new ActiveXObject("Microsoft.XMLHTTP");
        } catch (g) {}
        throw new Error("Jelo.Ajax.request: XMLHttpRequest not supported.");
    }();
    
    function loadScript(/* url */u, /* callback */fn) {
        var s = d.createElement("script"), e = d.documentElement;
        s.type = "text/javascript";
        if (s.readyState) {
            s.onreadystatechange = function() {
                if (s.readyState == "loaded" || s.readyState == "complete") {
                    s.onreadystatechange = null;
                    if (fn.isFunction) {
                        fn();
                    }
                }
            };
        } else {
            s.onload = function() {
                if (fn.isFunction) {
                    fn();
                }
            };
        }
        s.src = u;
        e.insertBefore(s, e.firstChild);
    }
    
    /** @scope Jelo.Ajax */
    return {
        
        /**
         * @returns True if an abortable {@link Jelo.Ajax.request} call is pending.
         * Pending requests made using {abortable: false} are not counted here.
         */
        isBusy     : function() {
            return ((x.readyState !== 0) && (x.readyState !== 4));
        },
        
        /**
         * Performs an AJAX call without XMLHttpRequest objects. Spiffy. Google
         * "jdomp", the guy has posted the idea on a bunch of web dev forums,
         * but I'm not sure if he has an official website. <br>
         * Note: parameters should be passed as a configuration object.
         *
         * @param {Object} config A configuration object.
         * @param {String} config.url Target URL to load (the script can be
         * dynamically generated by a server-side language, but the output
         * Content-Type must be text/javascript)
         * @param {Boolean} [config.cache=false] If false, add a timestamp to
         * the call to avoid caching the response. If true, it also assigns the
         * script a unique ID that can be referred to later.
         * @param {Object} [config.params] Additional parameters to pass to the
         * script via its query string. Not particularly useful for .js files,
         * but potentially useful if the script is served via PHP or another
         * server-side language.
         * @param {Function} [config.callback] Method to invoke after the
         * JDOMPed script executes. NOTE: Do NOT use nested callbacks to JDOMP
         * scripts with additional callbacks! If you do, callbacks beyond the
         * first will be executed multiple times!
         */
        jdomp      : function() {
            var config = arguments[0] || false;
            if (!config || !config.url) {
                return;
            }
            var cache = config.cache || false;
            var now = new Date().getTime();
            var params = config.params || null;
            var url = config.url;
            url += (cache) ? "?jdompCache=true" : "?jdompCache=" + now;
            for (var p in params) {
                if (params.hasOwnProperty(p)) {
                    url += "&" + escape(p) + "=" + escape(params[p]);
                }
            }
            var h = d.getElementsByTagName("head")[0] || false;
            var e = d.getElementById("script-jdomp") || false;
            if (e) {
                h.removeChild(e);
            }
            var s = d.createElement("script");
            s.id = "script-jdomp";
            s.type = "text/javascript";
            s.src = url;
            var alreadyExists = false;
            if (cache) {
                s.id += "-" + config.url.replace(/[^a-z]/gi, "");
                alreadyExists = d.getElementById(s.id) || false;
                if (alreadyExists) {
                    h.removeChild(alreadyExists);
                }
            }
            h.appendChild(s);
            if (typeof config.callback === "function") {
                var c = d.createElement("script");
                c.type = "text/javascript";
                c.id = s.id + "-callback";
                c.text = "new " + config.callback; // try changing to ['(', ')();'].join(config.callback)
                alreadyExists = d.getElementById(c.id) || false;
                if (alreadyExists) {
                    h.removeChild(alreadyExists);
                }
                h.appendChild(c);
            }
        },
        
        /**
         * Traditional AJAX request, supports (optional) caching. Parameters
         * should be passed in a configuration object.
         *
         * @param {Object} config A configuration object
         * @param {String} config.url The URL to send a request to.
         * @param {String} [config.method="GET"] GET, POST, PUT or DELETE.
         * @param {Object} [config.data] Parameters to pass to the URL.
         * @param {Object} [config.params] Alias for config.data
         * @param {Boolean} [config.cache=false] True to save the response in the
         * local cache, or retrieve the stored response if available. False to always
         * make a new request and return fresh results.
         * @param {Boolean} [abortable=false] If true, this call will interrupt any
         * pending AJAX requests which are also abortable. Each request that is NOT
         * abortable can execute simultaneously with other requests.
         * @param {Function} [config.success] Method to invoke when the request
         * successfully completed (200 or 304 HTTP status code). The function
         * gets passed the XMLHttpRequest object and the original config object.
         * In the callback function, "this" refers to the XMLHttpRequest object.
         * If the response Content-Type is text/xml, <strong>this.responseXML</strong>
         * should be available. Otherwise, get the response using
         * <strong>this.responseText</strong>.
         * @param {Function} [config.failure] Method to invoke when the request
         * was NOT successfully completed. The function gets passed the
         * XMLHttpRequest object and the original config object. In the callback
         * function, "this" refers to the XMLHttpRequest object. The status code
         * is available as <strong>this.status</strong>.
         * @param {Function} [config.callback]Method to invoke when the request
         * returns, <em>whether or not the call was successful</em>. Can be
         * useful for cleanup or notification purposes. The function gets passed
         * the XMLHttpRequest object and the original config object. In the
         * callback function, "this" refers to the XMLHttpRequest object. If
         * included, this method will be invoked AFTER both the success and
         * failure functions (if applicable).
         */
        request    : function() {
            var config = arguments[0] || {};
            if (!config || !config.url) {
                throw new Error("Jelo.Ajax.request: Required configuration option missing: url");
            }
            var x = config.abortable ? x : function() {
                try {
                    return new XMLHttpRequest();
                } catch (e) {}
                try {
                    return new ActiveXObject("Msxml2.XMLHTTP");
                } catch (f) {}
                try {
                    return new ActiveXObject("Microsoft.XMLHTTP");
                } catch (g) {}
                throw new Error("Jelo.Ajax.request: XMLHttpRequest not supported.");
            }();
            if ((x.readyState !== 0) && (x.readyState !== 4)) {
                // only one request at a time, new ones zap old ones
                x.abort();
            }
            var u = config.url;
            var p = config.params || config.data || {};
            var q = '';
            var m = (config.method || "GET").toUpperCase();
            var c = config.cache || false;
            var cs = config.success || false;
            var cf = config.failure || false;
            var cc = config.callback || false;
            var a = (config.args && config.args.isArray) ? config.args : [];
            var e = (/\?/).test(u); // existing url params
            var px = config.proxy;
            if (px) {
                u = px + "?url=" + u;
            }
            if ((m !== "GET") && (m !== "POST") && (m !== "PUT") && (m !== "DELETE")) {
                throw new Error("Jelo.Ajax.request: Method must be one of GET, POST, PUT, DELETE");
            }
            if (typeof p == "object") {
                for (var param in p) {
                    if (p.hasOwnProperty(param)) {
                        q += (q.indexOf('?') !== -1 ? '&' : '?') + Jelo.Format.urlencode(param) + "=" +
                            Jelo.Format.urlencode(p[param]);
                    }
                }
            } else if (typeof p == "string") {
                q += p; // raw post fields
            }
            var uCache = u + q; // don't include timestamp
            if (c) {
                var inCache = getFromCache(uCache, cs, cf, cc, config);
                if (!!inCache) {
                    return;
                }
            }
            
            // onreadystatechange
            var orsc = function() {
                if (x.readyState == 4) {
                    if (x.status === Status.OK) {
                        if (c) {
                            cache[uCache] = x.responseText;
                        }
                        if (cs.isFunction) {
                            cs.apply(x, [x, config]);
                        }
                    } else {
                        if (cf.isFunction) {
                            cf.apply(x, [x, config]);
                        }
                    }
                    if (cc.isFunction) {
                        cc.apply(x, [x, config]);
                    }
                }
            };
            
            switch (m) {
                case "GET" :
                    u += q;
                    u += (u.indexOf('?') !== -1 ? '&' : '?') + '_nocache=' + new Date().getTime();
                    x.open(m, u, true);
                    x.onreadystatechange = orsc;
                    x.setRequestHeader("X-Requested-With", 'XMLHttpRequest');
                    x.send(null);
                    break;
                case "POST" :
                    q = q.split("?", 2)[1];
                    x.open(m, u, true);
                    x.onreadystatechange = orsc;
                    x.setRequestHeader("X-Requested-With", 'XMLHttpRequest');
                    x.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
                    x.setRequestHeader("Content-length", q.length);
                    x.setRequestHeader("Connection", "close");
                    x.send(q);
                    break;
                default :
                    throw new Error("Jelo.Ajax.request: Method " + m + " not yet implemented.");
            }
        },
        /**
         * http://www.nczonline.net/blog/2009/06/23/loading-javascript-without-blocking/
         * 
         * @param {String} Address of the remote script. Should be of type text/javascript.
         * @param {Function} Callback function, to be executed after the script is done loading.
         */
        loadScript : loadScript
    };
}();
